﻿using System;

namespace Jayden.Dll.Core.IO
{

	public class Base64Stream : System.IO.Stream
	{
		private System.IO.Stream m_Stream;
		private byte[] m_Cache = new byte[3];
		private int m_CacheIndex = 0;
		private int m_BytesInCache = 0;

		private const string Base64IndexTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
		private static readonly byte[] Base64ReverseIndexTable = new byte[256];

		static Base64Stream()
		{
			if (Base64IndexTable.Length != 64)
				throw new Exception("Invalid Base64IndexTable length.");
			for (int c = 0; c < Base64IndexTable.Length; c++)
				Base64ReverseIndexTable[(int)Base64IndexTable[c]] = (byte)c;
		}

		public Base64Stream(System.IO.Stream stream) { m_Stream = stream; }

		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);
			m_Stream.Dispose();
			m_Cache = null;
		}

		public override bool CanRead { get { return m_Stream.CanRead; } }
		public override bool CanSeek { get { return false; } }
		public override bool CanWrite { get { return false; } }
		public override void Flush() { }

		public override long Length { get { throw new Exception("The method or operation is not implemented."); } }

		public override long Position
		{
			get { throw new Exception("The method or operation is not implemented."); }
			set { throw new Exception("The method or operation is not implemented."); }
		}

		public override int Read(byte[] buffer, int offset, int count)
		{
			int x = 0;
			// filling buffer with previous extracted data not used yet.
			if (m_CacheIndex != 0)
			{
				for (int c = m_CacheIndex; c < 3 && x < count; c++)
				{
					buffer[x + offset] = m_Cache[c];
					x++;
				}
				m_CacheIndex = 0;
			}
			if (x >= count)
				return x;
			int readBytesCount = count - x;
			int modulo3Count = (readBytesCount % 3);
			int bytesToPutInCache = 0;
			if (modulo3Count != 0)
			{
				bytesToPutInCache = 3 - modulo3Count;
				readBytesCount += bytesToPutInCache;
			}

			int base64BytesCount = ((readBytesCount / 3) * 4);

			byte[] charBuffer = new byte[base64BytesCount];
			int readInChunk = m_Stream.Read(charBuffer, 0, base64BytesCount);

			byte[] datas = new byte[readBytesCount];
			int tempBytes;
			int cacheIndexToUse = 2;
			byte value;
			bool over = false;
			for (int c = 0; c < readInChunk && !over; c += 4)
			{
				// extract 4 bytes base64 to 3 bytes int 
				value = charBuffer[c];
				tempBytes = (((int)Base64ReverseIndexTable[value]) & 0x3f) << 18;
				if (c + 1 < readInChunk)
				{
					value = charBuffer[c + 1];
					tempBytes |= (((int)Base64ReverseIndexTable[value]) & 0x3f) << 12;
					if (c + 2 < readInChunk)
					{
						value = charBuffer[c + 2];
						if (value == '=')
						{
							cacheIndexToUse = 0;
							over = true;
						}
						tempBytes |= (((int)Base64ReverseIndexTable[value]) & 0x3f) << 6;
						if (c + 3 < readInChunk)
						{
							value = charBuffer[c + 3];
							if (value == '=' && cacheIndexToUse == 2)
							{
								cacheIndexToUse = 1;
								over = true;
							}
							tempBytes |= (((int)Base64ReverseIndexTable[value]) & 0x3f);
						}
					}
				}
				// extract 3 bytes from int
				if ((tempBytes & 0xff000000) != 0)
					throw new Exception();
				m_Cache[0] = (byte)((tempBytes & 0xff0000) >> 16);
				m_Cache[1] = (byte)((tempBytes & 0xff00) >> 8);
				m_Cache[2] = (byte)(tempBytes & 0xff);
				// set bytes to buffer
				if (x < count)
				{
					buffer[x + offset] = m_Cache[0];
					x++;
					if (cacheIndexToUse > 0)
					{
						if (x < count)
						{
							buffer[x + offset] = m_Cache[1];
							x++;
							if (cacheIndexToUse > 1)
							{
								if (x < count)
								{
									buffer[x + offset] = m_Cache[2];
									x++;
									m_CacheIndex = 0;
								}
								else
									m_CacheIndex = 2;
							}
						}
						else
							m_CacheIndex = 1;
					}
				}
				else
					throw new Exception("Invalid amount of data read in the chunk.");
			}

			m_BytesInCache = bytesToPutInCache;

			return x;
		}

		public override long Seek(long offset, System.IO.SeekOrigin origin) { throw new Exception("The method or operation is not implemented."); }
		public override void SetLength(long value) { throw new Exception("The method or operation is not implemented."); }
		public override void Write(byte[] buffer, int offset, int count) { throw new Exception("The method or operation is not implemented."); }
	}

}
